using System;
using Bittorrent.Extentions;
using Bittorrent.Bencode.Collections;
using System.IO;
using System.Linq;
using Bittorrent.Bencode;
using System.Diagnostics.Contracts;
using Bittorrent.File.Exceptions;
using System.Collections.Generic;
using System.Security.Cryptography;

namespace Bittorrent.File
{
    public class Info
    {
        private readonly Dictionary _dictionary;

        /// <summary>
        /// Initializes a new instance of the Info class.
        /// </summary>
        public Info() {
        }

        internal Info(Dictionary info) {
            _dictionary = info;
            PieceLength = GetPieceLength(_dictionary);
            Pieces = GetPieces(_dictionary);
            IsPrivate = GetPrivate(_dictionary);
            Name = GetName(_dictionary);
            Length = GetLength(_dictionary);
            Files = GetTorrentFiles(_dictionary);
        }

        /// <summary>
        /// Gets number of bytes in each piece.
        /// </summary>
        public long PieceLength { get; set; }

        /// <summary>
        /// Gets all 20-byte SHA1 hash values, one per piece.
        /// </summary>
        public IEnumerable<byte[]> Pieces { get; set; }

        /// <summary>
        /// If true the client MUST publish its presence to get other peers. If false the
        /// client may obtain peer from other means.
        /// </summary>
        public bool IsPrivate { get; set; }

        /// <summary>
        /// If in single file mode (Files is empty) Name represents the filename of the file.
        /// If in multiple file mode (Files is not empty) Name represents the directory to store all the files.
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// The length of the file in bytes (if in single file mode else 0).
        /// </summary>
        public long Length { get; set; }

        public IEnumerable<MetaFile> Files { get; set; }

        /// <summary>
        /// Get the computed hash value of the encoded info class.
        /// </summary>
        /// <returns>The computed hash value.</returns>
        public byte[] GetHash() {
            using (SHA1 sha = new SHA1CryptoServiceProvider()) {
                return sha.ComputeHash(Bencode.BencodeFactory.Encode(_dictionary));
            }
        }

        internal Dictionary GetDictionary() {
            var dictionary = new Dictionary();

            dictionary.Add((ByteString)"piece length", new Integer { Value = PieceLength });
            
            // Each pieces should be 20bytes in size
            using (MemoryStream piecesStream = new MemoryStream(20 * Pieces.Count())) {
                foreach (var piece in Pieces) {
                    if (piece.Length != 20) {
                        throw new TorrentFileException("Piece length have to be 20bytes long to be valid");
                    }
                    piecesStream.Write(piece, 0, 20);
                }
                dictionary.Add((ByteString)"pieces", new ByteString { Bytes = piecesStream.ToArray() });
            }

            dictionary.Add((ByteString)"private", new Integer { Value = IsPrivate ? 1 : 0 });
            dictionary.Add((ByteString)"name", new ByteString { String = Name });
            var metaFiles = new List();
            foreach (var item in Files) {
                metaFiles.Add(item.GetDictionary());
            }
            dictionary.Add((ByteString)"files", metaFiles);
            dictionary.Add((ByteString)"length", new Integer { Value = Length });

            return _dictionary;
        }

        private static long GetPieceLength(Dictionary info) {
            if (!info.ContainsKey("piece length")) {
                throw new TorrentFileException("No piece length was found");
            }

            var pieceLength = info["piece length"] as Integer;
            if (pieceLength == null) {
                throw new TorrentFileException("Piece length is not represented by a Bencode.Integer");
            }

            return pieceLength.Value;
        }

        private static IEnumerable<byte[]> GetPieces(Dictionary info) {
            if (!info.ContainsKey("pieces")) {
                throw new TorrentFileException("Pieces was not found");
            }

            var pieces = info["pieces"] as ByteString;
            if (pieces == null) {
                throw new TorrentFileException("Pieces is not represented by a Bencode.ByteString");
            }

            if ((pieces.Length % 20) != 0) {
                throw new TorrentFileException("Wrong SHA1 pieces representation");
            }

            using (var memory = new MemoryStream(pieces.Bytes)) {
                var reader = new BinaryReader(memory);

                int i = 0;
                while (reader.BaseStream.Position < reader.BaseStream.Length) {
                    var piece = new byte[20];
                    reader.Read(piece, 0, 20);

                    yield return piece;

                    ++i;
                }
            }
        }

        private static bool GetPrivate(Dictionary info) {
            if (!info.ContainsKey("private"))
                return false;

            var isPrivate = info["private"] as Integer;
            if (isPrivate == null)
                return false;

            return isPrivate.Value == 1 ? true : false;
        }

        private static string GetName(Dictionary info) {
            if (!info.ContainsKey("name")) {
                throw new TorrentFileException("No info name field found");
            }

            var name = info["name"] as ByteString;
            if (name == null) {
                throw new TorrentFileException("name is not represented by a Bencode.ByteString");
            }

            return name.String;
        }

        private static IEnumerable<MetaFile> GetTorrentFiles(Dictionary info) {
            if (!info.ContainsKey("files"))
                yield break;

            var files = info["files"] as List;
            if (files == null)
                yield break;

            foreach (var item in files) {
                if (!(item is Dictionary)) {
                    throw new TorrentFileException("files list can only contain Bencode.Dictonary's");
                }

                yield return new MetaFile((Dictionary)item);
            }
        }

        private static long GetLength(Dictionary info) {
            if (!info.ContainsKey("length")) {
                return 0;
            }

            var length = info["length"] as Integer;
            if (length == null) {
                return 0;
            }

            return length.Value;
        }
    }
}